feat(notices): address a notice to a lineage conversation or root (#458) - #539
Conversation
🦋 Changeset detectedLatest commit: ab25b96 The changes in this PR will be included in the next version bump. This PR includes changesets to release 2 packages
Not sure what this means? Click here to learn what changesets are. Click here if you're a maintainer who wants to add another changeset to this PR |
Codex Review SummaryThis comment shows the latest Codex review activity on this pull request.
ℹ️ About Codex in GitHubYour team has set up Codex to review pull requests in this repo. Reviews are triggered when you
Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings. |
commit: |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: b26dd5ffec
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| @@ -0,0 +1,6 @@ | |||
| --- | |||
| "@agent-bundle/runtime": patch | |||
There was a problem hiding this comment.
Mark the required lineage field as breaking
Publishing @agent-bundle/runtime as a patch is unsafe because this commit makes AgentNoticePrincipal.lineage required in the public API. Existing TypeScript consumers constructing the previous four-axis principal will stop compiling, while existing JavaScript consumers of AgentNoticeLedger.openRequest() can reach recordedNoticePrincipal() and throw when it reads principal.lineage.state. Declare the runtime change as minor, or preserve compatibility by making lineage optional and treating omission as unavailable.
AGENTS.md reference: AGENTS.md:L102-L103
Useful? React with 👍 / 👎.
f787c44 to
710fd25
Compare
710fd25 to
530997a
Compare
recipient.conversation matches request.lineage.conversation exactly and
recipient.root matches every request whose lineage root is that id, in
conjunction with the existing actor/host/session/workspace axes. The notice
principal carries the request's lineage on every generated surface; the
ledger journals only { conversation, root } of the admitting lineage as an
additive optional field (no definition version bump).
examples/worktree-proximity addresses proximity notices to the other actor's
conversation instead of its worktree; the derived worktree:<root> actor keeps
the workspace axis.
… principals keep working Absent lineage reads as unavailable: nothing journaled, no conversation/root match, every other axis unchanged. Test covers a four-axis principal handed straight to openRequest.
530997a to
06f7754
Compare
Closes #458.
Summary
A notice can now be addressed to one agent conversation or to a whole conversation tree, matched against the admitting request's
request.lineage(#444: lineage is the identity surface; #391: no actor derivation).AgentRecipient(packages/rsc-runtime/src/notices/contract.ts) gains two optional axes, additive to the existing four (every present axis must match):conversation?: string— matches whenrequest.lineage.conversation === conversation(exactly one agent thread: the root, or one subagent — Claude/Codexagent_id, Cursor childconversation_id).root?: string— matches whenrequest.lineage.root === root(the root conversation and every subagent under it; a pure match, so the publisher is included when it is under that root).AgentNoticePrincipalgains an optionallineage?: Observed<AgentLineage>;runAgentRequestmounts it (agent-request.ts), so every generated surface — event routes, MCP tools (registry-correlated), routed CLI, rendered scripts, standalone hooks (resolveStandaloneLineage) — carries it. Unavailable lineage matches noconversation/rootrecipient (fail closed), exactly like an unavailable axis today.resources/subscriberecordslineage: unavailable('not-provided')(no correlation window), so a subscriber never matches a lineage-addressed notice;GeneratedNoticePrincipaland the test harness'swithContextIdentitycarry the axis.state.ts,ledger.ts):recipientMatchesPrincipalchecks the two axes;recordedNoticePrincipalprojects the admission's principal to{ conversation, root }so the journaledadmittedpayload never depends on the rest of theAgentLineageshape (relevant to Expose the lineage tree (siblings, children, live roots) to routes, not only the request's own chain #457, which may widen it).publish()rejects blankconversation/rootwithinvalid-input.recipient.conversation,recipient.root, andprincipal.lineageonadmittedare optional schema fields without defaults, so stored heads parse unchanged and the head-vs-replay check cannot diverge. An admission journaled before the axes carries no lineage and matches exactly the recipients it matched when written (no persisted notice names an axis it did not have), so the reducer does not change what any already-journaled event means — thestate.tsv2 comment documents this.AGENT_NOTICE_STATE_VERSIONstays 2; a test dispatches a pre-Let notices target a lineage conversation: recipient.conversation / recipient.root matched against request.lineage #458admittedpayload against the current definition.examples/worktree-proximity:tool/before.tsxpublishesrecipient: { conversation: <other actor's conversation> }(noticeRecipientForinevent-support.ts); the derivedworktree:<root>fallback actor names no conversation and keepsrecipient.workspace.root.childFromLineageuseslineage.conversationas the actor id (identical tosubagent.idon the example's targets, Claude and Codex). README updated.website/plugins/generated-reference.ts); runtime README notices section;docs/entry-conventions.md. Changeset:.changeset/458-lineage-notice-recipients.md(patch, additive).Design question: a
send(toConversation, notice)façadeNot added. The #99 host survey (2026-09-02) records that no pinned host offers a directed push channel (
directed-pushunavailable on claude/codex/cursor/portable), so any façade would be sugar over exactly what exists:publish()followed by admission on the recipient conversation's next event. The new axes makepublish()expressive enough —{ conversation }for one peer,{ root }for the tree, conjoined withworkspace/sessionwhen wanted. Implementing this did not change my view: the example needed one line (recipient), and a façade would have had to invent a delivery claim the ledger deliberately does not make (attemptedstays the strongest cross-actor state). If an app wantssend(), it is a two-line app-level helper.Notes for reviewers
packages/agent-bundle/tests/worktree-proximity-journeys.test.ts): the fixture previously omittedagent_idfrom the children's tool hook payloads and had no spawningAgentPreToolUse, so the lineage registry could not place the children (id-not-resolvable) and a conversation-addressed notice had nothing to match. Real Claude does both (127 hook payloads in the docs(lineage): live Claude Code 2.1.259 orchestration capture (parallel, sequential, nested spawns; resumed turns; /compact) #455 capturefixtures/host-lineage/claude-2.1.259-orchestration.ndjson), so the suite now runs the root'sAgentPreToolUsebefore eachSubagentStartand putsagent_idon the children's tool payloads. Consequences: child provenance becomesregistry(placed by the runtime), and the suite additionally proves that an event the runtime cannot place under agent-a is not delivered to. Workspace-directed delivery (the derived-actor fallback) is proven in the example's route-unit suite instead.agent-c) working in the recipient's worktree, nor on an unresolved-lineage event there, and is admitted on the addressed agent's event. The domain only flags cross-worktree overlap, so the conflict itself is still produced across worktrees.feat/457-lineage-tree): nolineage/*files touched; the ledger stores a projection of the lineage, so wideningAgentLineagethere does not affect journaled payloads.Tests
packages/rsc-runtime/tests/notices-ledger.test.ts— newlineage-addressed recipients (#458)block: per-axis match/no-match matrix and conjunctions; unavailable lineage never matches; legacy principal without lineage; recorded-principal projection; conversation-addressed admission skips sibling/root/unresolved and lands on the addressed conversation; root-addressed admission reaches every member of the subtree and nothing outside; inbox and acknowledge scoped by conversation; blank axes rejected; pre-Let notices target a lineage conversation: recipient.conversation / recipient.root matched against request.lineage #458admittedpayload accepted under version 2.packages/agent-bundle/tests/route-unit/lineage-notices.test.ts— replays the Claude 2.1.259 orchestration (docs(lineage): live Claude Code 2.1.259 orchestration capture (parallel, sequential, nested spawns; resumed turns; /compact) #455), Codex 0.147.0, and Cursor 3.18.25 captures through the lineage registry and renders event routes with those lineages: parent → one child (conversation), sibling and root excluded; child → tree (root) reaches every member; unresolved lineage never admits.examples/worktree-proximity/tests/route-unit/routes.test.ts— journeys 4–6 now assertrecipient: { conversation: 'agent-a' }; new: one agent in a shared worktree; derived-actor workspace fallback.packages/agent-bundle/tests/worktree-proximity-journeys.test.ts— see above.Verification (re-run on the rebased head
710fd2552)pnpm typecheckclean;pnpm lint0 errors / 0 warningspnpm test:unit3263 passed;pnpm test:route-unit73 passed (incl. the new file)packages/rsc-runtime:rstest tests/notices-*.test.ts99 passedexamples/worktree-proximity:pnpm typecheck,pnpm test:routes12 passedrstest --config rstest.integration.config.ts packages/agent-bundle/tests/worktree-proximity-journeys.test.tspassedpnpm docs:site:buildclean (dead-link, anchor, image, language-parity checks)Review status
b26dd5ffe— one P1 thread on.changeset/458-lineage-notice-recipients.md: a requiredAgentNoticePrincipal.lineagewould break four-axisopenRequest()/subscribe()callers (TypeScript consumers stop compiling; JavaScript consumers reachrecordedNoticePrincipal()and throw). Fixed inf787c4462by the compatible option the thread offered:lineageis optional onAgentNoticePrincipalandGeneratedNoticePrincipal; absent reads as unavailable (nothing journaled, noconversation/rootmatch, every other axis unchanged). New ledger tests cover a four-axis principal throughrecipientMatchesPrincipal,recordedNoticePrincipal, andopenRequest()admission. Changeset stayspatch(additive). Answered here rather than on-thread by instruction.b26dd5ffe. Unreviewed heads:f787c4462(only the optional-lineagefix above plus its tests and wording),710fd2552(the same two commits rebased ontomainat feat(test): mountTestState/withTestState — one mounted state across several renderRoute calls #525, resolving the example route-unit test ontomountTestState; no other change),530997a16(rebased ontomainat ci: run required checks on merge_group for the merge queue #536), and06f7754de(rebased ontomainat ci: add a static-named Verify gate job for the required check #549 so the new requiredVerify gatecontext runs; the branch-protectionstrictrule requires an up-to-date head). No source change in either rebase. No fresh review arrived within the fallback window and no PR comments are posted from this lane by instruction, so this merges on green CI per the reviewer-quota fallback; a later thread on the merged PR is answered in a follow-up PR.